package com.db.stockexchange.network;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;


public class Client implements Runnable {
	private static final int DEFAULT_SELECT_TIMEOUT = 100;

	private static final String DEFAULT_HOST = "localhost";
	
	private AtomicBoolean running = new AtomicBoolean(false);
	private SocketChannel sc;
	private Selector selector;
	private MessageTransceiver transceiver;
	private List<ResponseListener> responseListeners = new LinkedList<>();
	private List<DisconnectListener> disconnectListeners = new LinkedList<>();
	private Logger logger = Logger.getLogger(Client.class.getName());
	
	public Client(int port, String host) throws IOException {
		logger.info("client connecting to " + host + ":" + port);
		selector = Selector.open();
		sc = SocketChannel.open();
		sc.configureBlocking(true);
		sc.connect(new InetSocketAddress(host, port));
		sc.configureBlocking(false);
		sc.register(selector, SelectionKey.OP_READ);
		transceiver = new MessageTransceiver();
		logger.info("connection successful");
	}
	
	public Client(String host) throws IOException {
		this(Server.DEFAULT_PORT, host);
	}
	
	public Client() throws IOException {
		this(Server.DEFAULT_PORT, DEFAULT_HOST);
	}
	
	public boolean isRunning() {
		return running.get();
	}
	
	public boolean stop() {
		return running.compareAndSet(true, false);
	}
	
	public void addResponseListener(ResponseListener listener) {
		responseListeners.add(listener);
	}
	
	public void addDisconnectListener(DisconnectListener listener) {
		disconnectListeners.add(listener);
	}
	
	@Override
	public void run() {
		
		if (!running.compareAndSet(false, true)) {
			logger.warning("called run(), but client is already running");
			return;
		}
		
		try {

			
			while (running.get()) {
				selector.select(DEFAULT_SELECT_TIMEOUT);
				Set<SelectionKey> selectedKeys = selector.selectedKeys();
				for (SelectionKey sk : selectedKeys) {
					if (sk.isReadable())
						read(sk);
				}
				selectedKeys.clear();
			}
		} catch (IOException e) {
			logger.severe("client failure: " + e.getMessage());
			running.set(false);
			onDisconnect();
		}
	}
	
	private void onDisconnect() {
		for (DisconnectListener listener : disconnectListeners) {
			listener.onDisconnect();
		}
	}

	public void close() throws IOException {
		logger.warning("closing");
		selector.close();
		sc.close();
		running.set(false);
	}

	public void write(byte[] message) throws IOException {
		transceiver.transmit(message, sc);
	}
	
	private void read(SelectionKey sk) throws IOException {
		transceiver.receive(sc);
		
		byte[] message;
		while ((message = transceiver.nextMessage()) != null) {
			for (ResponseListener listener : responseListeners) {
				listener.responseReceived(message);
			}
		}
	}
}
